Skip to content

fix(ai): real endpoints, --agent AI mode, claude auth errors, keyless auto-pick, an isolated claude -p - #850

Merged
fas89 merged 6 commits into
mainfrom
fix/copilot-keyless-defects
Nov 12, 2026
Merged

fas89 merged 6 commits into
mainfrom
fix/copilot-keyless-defects

Conversation

@fas89

@fas89 fas89 commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

A review of forge's AI authoring path (2026-11-06) found five defects, all reproduced on origin/main:

  1. LlmConfig.endpoint held the telemetry label litellm://<provider>/<model> whenever no endpoint was set.
    • Ollama sent it to litellm as api_base (litellm://ollama/gemma3:4b), so no local call ever reached the daemon.
    • fluid ai test posted to it and failed with "AI test network error for ollama (UnsupportedProtocol)".
  2. fluid forge --agent forced --blank unless a mode flag was given. --agent --llm-provider claude-code --context '<intent>' wrote description: 'TODO: Describe your data product' / sql: SELECT 1 AS placeholder and emitted forge.done exit_code 0, which an IDE agent reads as success.
  3. claude -p --output-format json reports a failure on stdout (is_error: true, e.g. "Failed to authenticate: OAuth session expired and could not be refreshed") and leaves stderr empty. The not-authenticated check read stderr only, so users saw "claude-code exited with code 1:" and nothing else.
  4. With nothing configured and no TTY, forge fell back to gemini even when a logged-in Claude Code was installed.
  5. claude -p ran with Claude Code's full agent prompt plus the user's own hooks, plugins, MCP servers and CLAUDE.md: 47,258 prompt tokens for a one-line call.

Founder decisions:

  • --agent honors the AI flags.
  • The auto-pick applies only to non-interactive runs, and only after a running local Ollama, so local data never silently moves to the cloud.
  • Model defaults are unchanged here, because Command Center passes no --llm-model.
  • No backward-compatibility shims.

The change

  • 8fa6a051 fix(coding-agent) (3, 5): failures are read from the agent's stdout result. is_error: true fails even at exit code 0, and a login problem raises coding_agent_not_authenticated with the sign-in hint. For claude-code only:

    Pattern from claude-agent-sdk-python (MIT) @ 3cf3446b0f, subprocess_cli.py::_build_command.

  • f2aac8ed fix(llm) (1, 4):

    • LlmConfig.endpoint holds only a real address: the stated one, else OLLAMA_HOST, else litellm's own default.
    • OLLAMA_HOST is parsed the way ollama-python's _parse_host (MIT) parses it: a bare host means :11434.
    • Routing calls keep a stated endpoint (endpoint_explicit).
    • fluid ai test probes through the provider call and exits 4 on a 404.
    • Doctor shows "Provider default".
    • Keyless auto-pick, one shared helper for selection, readiness and preflight:
      • Order: a running Ollama, then the first agent CLI on the environment's own PATH (claude-code, codex, cursor, kiro).
      • Blocked by any key or provider key variable, FLUID_LLM_[ROUTING_]ENDPOINT/MODEL, OLLAMA_HOST, a keyring key, a saved provider, --llm-endpoint/--llm-model (and the routing twins), or the agent loop (flag or FLUID_COPILOT_AGENT_LOOP).
      • The litellm preflight lets it through on the base install.
      • Readiness calls an agent ready only when its CLI is on PATH.
  • 331f6803 fix(forge) (2): --llm-provider/--llm-model/--llm-endpoint/--context select AI mode under --agent, run non-interactively. AI-mode runs now emit forge.contract_written: the receipt path compared diff_snapshots' create/update against created/modified, so it never fired.

  • fadbdbf5: CHANGELOG [Unreleased] (Fixed, Changed).

Review

  • Borrow-before-build:

    • claude-agent-sdk-python subprocess_cli.py + CHANGELOG;
    • litellm's Ollama default (llms/ollama/common_utils.py);
    • ollama-python _parse_host.

    Pattern adapted, no new dependency.

  • Security review (diff-scoped), 1 finding, fixed and re-checked: a stated --llm-endpoint / --llm-model / FLUID_LLM_* or provider key did not count as configured. A non-interactive run, --agent --llm-endpoint http://onprem-llm.internal:8000/v1 included, therefore sent the prompt to claude-code. The routing endpoint/model variant is closed too.

  • Defect scan (diff-scoped, reproduced), 3 P2 + 3 P3, all fixed and re-checked:

    • --agent-loop auto-picking an agent that has no tool use;
    • for_routing() dropping a stated Ollama endpoint;
    • doctor "Ready" for an uninstalled agent;
    • OLLAMA_HOST passing the lean preflight;
    • bare-host port 80;
    • ai test 404 exiting 5.
  • Accepted residual: on the base install, a keyring-only key passes the preflight (which reads no keyring by design) and fails cleanly at resolve time.

Testing

  • New: tests/test_llm_endpoint_sentinel.py, tests/test_llm_keyless_autopick.py, tests/test_coding_agent_isolation.py, plus additions in test_litellm_absent.py, cli/test_forge_agent_mode.py and test_coding_agent_provider.py.
  • Every test file that touches the changed modules: 5945 passed. The 2 failures (tests/cli/test_lazy_core_commands.py) fail the same way on pristine main in a shared venv.
  • Lint & Format (ruff, the dead-code sweep, black 24.10.0, product guardrail, detect-secrets), license headers and lint-imports: green.
  • fluid --help loads the same 1028 modules on main and on the branch, with none of litellm/httpx/mcp/jsonschema.
  • Live, through the fluid CLI:
    • fluid ai test --provider ollama --model qwen3-coder:30b: main ok: false (UnsupportedProtocol); branch ok: true (FLUID_OK) with OLLAMA_HOST unset, localhost and 127.0.0.1:11434.
    • fluid forge --agent --llm-provider ollama --llm-model qwen3-coder:30b --context <intent>: mode ai, valid contract, 107 s.
    • fluid forge --agent --llm-provider claude-code --llm-model sonnet --context <intent>: mode ai, forge.contract_written, a valid contract scoring 10/12 on a fixed fidelity checklist, 57 s, 2 calls, $0.17.
    • claude -p --json-schema with the isolation flags still returns structured_output. The same call went from 47,258 to 1,301 prompt tokens and from 4.2 s to 1.3 s.
  • Command Center: fluid forge data-model from-intent ... --llm-provider {openai|anthropic|claude|gemini}, with the key in env and no --llm-model, resolves the same provider, model and routing model as main. Only the endpoint changes, from litellm://… to "".

Documentation

Not in this PR

  • Stale model defaults (catalog dated 2026-04-24): waits until Command Center passes --llm-model.
  • Two pre-existing defects, for a separate PR: non-interactive runs ignore the saved fluid ai setup provider, and --agent stdout mixes rich panels into the JSONL stream.

…part from the user's setup

claude -p reports a failure on stdout as a JSON result with
is_error: true and leaves stderr empty, so the not-authenticated check
(stderr only) never fired and the error read "claude-code exited with
code 1:" with no reason. The provider now reads that result: the auth
markers are matched against it plus stderr, any other failure carries
the agent's own redacted message, and is_error: true fails even at
exit code 0.

Each claude-code call now replaces Claude Code's agent system prompt
(--system-prompt), passes --tools "", --strict-mcp-config,
--setting-sources= and --no-session-persistence, and drops CLAUDECODE
from the child env, so the user's hooks, plugins, MCP servers and
CLAUDE.md no longer reach contract generation. Each flag is used only
when the installed claude --help lists it (probed once per process),
so older CLIs keep working. Pattern from claude-agent-sdk-python (MIT)
@ 3cf3446b0f, subprocess_cli.py _build_command (SDK #778, #732).
…agent when nothing is configured

LlmConfig.endpoint carried the telemetry label
litellm://<provider>/<model> whenever no endpoint was set. Ollama sent
it to litellm as api_base, so no call reached the daemon, and
fluid ai test posted to it and failed with UnsupportedProtocol. The
endpoint now holds only a real address (the stated one, else
OLLAMA_HOST, else litellm's own default). OLLAMA_HOST is read as
ollama-python's _parse_host reads it (MIT), routing calls keep a stated
endpoint, fluid ai test probes through the provider call and exits 4
on a 404, and doctor shows "Provider default".

A non-interactive run with nothing configured now uses a running local
Ollama first, else the first coding-agent CLI on PATH (claude-code,
codex, cursor, kiro), and logs which. A key, a keyring entry, a saved
provider, OLLAMA_HOST, or a stated endpoint or model (flags or env,
the routing ones too) blocks it, as does the agent loop. The litellm
preflight lets it through on the base install, where the agents are
the only provider that can run. Readiness reports an agent ready only
when its CLI is on PATH.
…tract it wrote

fluid forge --agent forced --blank unless a mode flag was given, so
--llm-provider, --llm-model, --llm-endpoint and --context were dropped
and the run wrote a TODO contract with exit_code 0, which an IDE agent
reads as success. Those flags now select AI mode, run
non-interactively, and a failed generation exits non-zero.

The receipt path compared diff_snapshots actions against
"created"/"modified" while it emits "create"/"update", so AI-mode runs
never sent forge.contract_written. The actions are now mapped to the
blank path's vocabulary.
@github-actions github-actions Bot added cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes needs-docs Pull request needs a linked docs update or justification labels Nov 11, 2026
@github-actions

Copy link
Copy Markdown

📄 Documentation Reminder

This PR appears to be missing a documentation reference. Our docs live in a separate repo.

Please update the PR description with one of:

  • Link a docs PR — check the "Docs PR linked" box and paste the URL
  • Mark as no docs needed — check "No docs needed" with a justification
  • Acknowledge docs TODO — check "Docs TODO" and create the docs PR before merge

See the Contributing Guide for details.

@github-actions github-actions Bot removed the needs-docs Pull request needs a linked docs update or justification label Nov 11, 2026
…numbers

The lean-install preflight test sets GEMINI_API_KEY to a dummy value,
which detect-secrets flags as a Secret Keyword. It now carries the
inline allowlist pragma the other tests use. The baseline's line
numbers for already-audited fixtures in the touched test files moved
with this PR's edits; detect-secrets-hook rewrote them.
@github-actions github-actions Bot added the security Security-related changes or reports label Nov 11, 2026
litellm 1.109.0 (released 2026-11-11) reads fourteen more
<PROVIDER>_API_BASE names: ABLITERATION, AIAND, BIGMODEL,
CLF_AI_GATEWAY, EU_JEV, HOSTINGER_ROUTER, KYMA, NACE, OPENCODE_GO,
OPENINFER, OPENSEARCH, REFUSELESS, STREAMLAKE and ZEROGPU. Each comes
from litellm.provider_list, llms/openai_like/providers.json or the
provider's own module, so each picks a provider host and joins
LITELLM_ENDPOINT_NAMES. tests/test_litellm_endpoint_names.py failed on
every CI leg that installs it (Python 3.11 and up; 3.10 stays below
1.98). It passes on litellm 1.109.0 and 1.104.0.
@fas89
fas89 merged commit 28a1f2e into main Nov 12, 2026
37 checks passed
fas89 added a commit that referenced this pull request Nov 12, 2026
…y host, no gemini fallback (#852)

* fix(forge): --agent reports a contract written to a --target-dir outside the cwd

The receipt and the forge.contract_written event come from a before/after
snapshot that scanned only the current directory, two levels deep. A
--target-dir outside it, or deeper, got neither: an AI-mode run wrote the
contract and the JSONL stream never said so. The stated target is now
snapshotted too, with ~ expanded as the writer expands it, and a file outside
the current directory is reported by its absolute path.

Found by the forge_docs review of #850.

* fix(ai): one Ollama address for every read, any host, from the stated endpoint

OLLAMA_HOST was read four ways. The chat call dialled any host, but detection,
forge's default-model pick and fluid ai setup ignored a non-local host with a
warning and asked localhost, so the model came from one daemon and the call
went to another. fluid ai test --endpoint refused a non-local Ollama outright.
Owner decision: any host, as in ollama-python. OLLAMA_HOST is already a
protected name that a project .env cannot set.

- fluid_build/llm/ollama_host.py is the one reader, following ollama-python's
  _parse_host convention (MIT). It keeps user:password@, sent as basic auth,
  and reads the scheme case-insensitively.
- A stated endpoint (--llm-endpoint, FLUID_LLM_ENDPOINT, ai test --endpoint)
  now drives the default-model pick, the model preflight and the ai test model
  check, which asked OLLAMA_HOST or localhost. A --tiered stage keeps a stated
  endpoint, as LlmConfig.for_routing already did.
- LlmProvider.list_available_models takes an endpoint keyword, passed only when
  one is stated. The Ollama model list moved from urllib to httpx, so basic
  auth works and no redirect is followed; a non-http(s) URL is refused.
- Credentials: fluid doctor and fluid ai setup show the address masked
  (providers.redact_endpoint), ai_config.json stores it without them unless
  FLUID_ALLOW_PLAINTEXT_AI_SECRETS is set, and the environment's OLLAMA_HOST
  wins over the saved host.

* fix(ai): stop with copilot_no_llm_provider instead of guessing gemini

With nothing selecting a provider, forge used gemini. It sent
FLUID_LLM_API_KEY to Google whatever key it was, and with two provider keys
set it asked for a gemini key, while fluid doctor said "Not selected". Owner
decision: no silent fallback.

resolve_llm_config now raises copilot_no_llm_provider before any provider is
built, with no_provider_message(env), shared with check_llm_readiness so forge
and doctor say the same thing: nothing configured, several keys set, a key that
names no provider, or a saved fluid ai setup choice the command does not read.
The error catalog carries the suggestions.

* test(ai): reset the setup-skip flag in the inline-setup credentials test

Coverage & Audit (Python 3.14) failed on test_inline_setup_keeps_the_credentials_in_ollama_host
with "'NoneType' object has no attribute 'provider'": run_ai_setup_inline returns None when an
earlier test on the same worker left the module-level _ai_setup_skipped set, and random test
order put one first. The test now resets the flag, as the other inline-setup tests do, points
HOME at its tmp dir and pins the litellm check, so it reads nothing of the machine it runs on.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Changes to the CLI surface or implementation security Security-related changes or reports tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant